I'm trying to understand Ruby singletons and class inheritance better. I read everywhere that
def self.method_name; end`
is equivalent to
class << self
def method_name; end
end
But if that were true, then I would expect print_constant_fails to work, but it doesn't. What is going on here?
class SuperExample
A_CONSTANT = "super example constant"
end
class SubExample < SuperExample
def self.print_constant_works_1
puts A_CONSTANT
end
class << self
def print_constant_works_2
puts self::A_CONSTANT
end
def print_constant_fails
puts A_CONSTANT
end
end
end
pry(main)> SubExample.print_constant_works_1
super example constant
pry(main)> SubExample.print_constant_works_2
super example constant
pry(main)> SubExample.print_constant_fails
NameError: uninitialized constant #<Class:SubExample>::A_CONSTANT
from (pry):13:in `print_constant_fails'
It has to do with scope. When you are inside class << self, the scope is different than when you are inside class Something. Thus, inside class << self there is actually no constant called A_CONSTANT.
You have encountered a common Ruby gotcha - constant lookup.
The most important concept in constant lookup is Module.nesting (unlike in method lookup, where the primary starting point is self). This method gives you the current module nesting which is directly used by the Ruby interpreter when resolving the constant token. The only way to modify the nesting is to use keywords class and module and it only includes modules and classes for which you used that keyword:
class A
Module.nesting #=> [A]
class B
Module.nesting #=> [A::B, A]
end
end
class A::B
Module.nesting #=> [A::B] sic! no A
end
In meta programming, a module or class can be defined dynamically using Class.new or Module.new - this does not affect nesting and is an extremely common cause of bugs (ah, also worth mentioning - constants are defined on the first module of Module.nesting):
module A
B = Class.new do
VALUE = 1
end
C = Class.new do
VALUE = 2
end
end
A::B::VALUE #=> uninitialized constant A::B::VALUE
A::VALUE #=> 2
The above code will generate two warnings: one for double initialization of constant A::VALUE and a second for reassigning the constant.
If it looks like "I'd never do that" - this also applies to all the constants defined within RSpec.describe (which internally calls Class.new), so if you define a constant within your rspec tests, they are most certainly global (unless you explicitly stated the module it is to be defined in with self::)
Now let's get back to your code:
class SubExample < SuperExample
puts Module.nesting.inspect #=> [SubExample]
class << self
puts Module.nesting.inspect #=> [#<Class:SubExample>, SubExample]
end
end
When resolving the constant, the interpreter first iterates over all the modules in Module.nesting and searches this constant within that module. So if nesting is [A::B, A] and we're looking for the constant with token C, the interpreter will look for A::B::C first and then A::C.
However, in your example, that will fail in both cases :). Then the interpreter starts searching ancestors of the first (and only first) module in Module.nesting. SubrExample.singleton_class.ancestors gives you:
[
#<Class:SubExample>,
#<Class:SuperExample>,
#<Class:Object>,
#<Class:BasicObject>,
Class,
Module,
Object,
Kernel,
BasicObject
]
As you can see - there is no SuperExample module, only its singleton class - which is why constant lookup within class << self fails (print_constant_fails).
The ancestors of Subclass are:
[
SubExample,
SuperExample,
Object,
Kernel,
BasicObject
]
We have SuperExample there, so the interpreter will manage to find SuperExample::A_CONSTANT within this nesting.
We're left with print_constant_works_2. This is an instance method on a singleton class, so self within this method is just SubExample. So, we're looking for SubExample::A_CONSTANT - constant lookup firstly searches on SubExample and, when that fails, on all its ancestors, including SuperExample.
In Ruby, every Ruby's constant has its own path, start from the main (root) with the sign :: (default, we don't need to declare this sign). And class should not be considered a keyword (kind of static), but a method (kind of dynamic) take responsibility for creating a class object and a class name constant which point to that class object, and all Constants are defined inside a class without a path (P::Q::...) will automatically be considered belongs to the created class with path :: ClassName::A_CONSTANT.
GLOBAL = 1
class SuperExample
A_CONSTANT = "super constant" # <-- ::SuperExample::A_CONSTANT
end
puts ::GLOBAL # 1
puts ::SuperExample # SuperExample
puts ::SuperExample::A_CONSTANT # "super constant"
It looks like constants paths in children classes has same level with parent
class SubExample < SuperExample
end
puts ::SubExample::A_CONSTANT # "super constant"
As I noticed, all constants (without ::) inside the class block will be set path under the classpath, so when you get them, either you get with the explicitly constant path or under the class that constants belong to:
class SubExample < SuperExample
def self.print_constant_works_1
puts A_CONSTANT # ::SubExample::A_CONSTANT
end
def another
puts A_CONSTANT # ::SubExample::A_CONSTANT
end
def yet_another
puts SubExample::A_CONSTANT # ::SubExample::A_CONSTANT
end
end
Now check class << self
class SubExample < SuperExample
class << self
puts self # <Class:SubExample>
def print_constant_works_2
puts self::A_CONSTANT # declare explicitly constant path
end
def print_constant_fails
puts A_CONSTANT # not declare explicitly <-- Class:SubExample::A_CONSTANT
end
end
end
As you can see, the class inside class << self is different, so the path of constant A_CONSTANT inside method print_constant_fails is pointing to Class:SubExample which does not define any constant A_CONSTANT, so an error uninitialized constant #<Class:SubExample>::A_CONSTANT be raised.
Meanwhile print_constant_works_2 will work since we declare explicitly constant path, and self in this case is actually SubExample(call SubExample.print_constant_works_2).
Now let try with an explicit path ::A_CONSTANT inside print_constant_fails
def print_constant_fails
puts ::A_CONSTANT
end
The error be raised is uninitialized constant A_CONSTANT, ::A_CONSTANT is considered a global constant (main).